#include "mpLog.h"
#include "mpProcessUtils.h"
#include "mpFileUtils.h"

#include <string.h>
#include <vector>
#include <iostream>

#ifdef PLATFORM_WINDOWS

#else
#include <stdlib.h>
#include <sys/wait.h>
#include <errno.h>
#endif

#ifdef PLATFORM_FREEBSD
#include <sys/sysctl.h>
#include <sys/types.h>
#endif

#ifdef PLATFORM_MAC
#include <Security/Security.h>
#include <mach-o/dyld.h>
#endif

#include <boost/lexical_cast.hpp>


namespace MP{
namespace ProcessUtils{

//private member
namespace{

/**
 * @brief The RunMode enum
 */
enum RunMode
{
    RunSync,
    RunAsync
};

#ifdef PLATFORM_WINDOWS

/**
 * @brief quoteArgs 将参数用空格符拼接起来。如果参数中包含空格符则参数会用括号括起来
 * @param arguments
 * @return
 */
std::string _quoteArgs(const std::list<std::string>& arguments)
{
    std::string quotedArgs;
    for (std::list<std::string>::const_iterator iter = arguments.begin();
         iter != arguments.end();
         iter++)
    {
        std::string arg = *iter;

        bool isQuoted = !arg.empty() &&
                         arg.at(0) == '"' &&
                         arg.at(arg.size()-1) == '"';

        if (!isQuoted && arg.find(' ') != std::string::npos)
        {
            arg.insert(0,"\"");
            arg.append("\"");
        }
        quotedArgs += arg;
        quotedArgs += " ";
    }
    return quotedArgs;
}


/**
 * @brief 将路径中的 / 转换成 \\
 * @param str
 * @return
 */
std::string _toWindowsPathSeparators(const std::string& str){
    std::string result = str;
    std::replace(result.begin(),result.end(),'/','\\');
    return result;
}


#endif //PLATFORM_WINDOWS

#ifdef PLATFORM_UNIX

PROCESS_PID _runAsyncUnix(const std::string& executable,
                        const std::list<std::string>& args)
{
    pid_t child = fork();
    if (child == 0)
    {
        // in child process
        char** argBuffer = new char*[args.size() + 2];
        argBuffer[0] = strdup(executable.c_str());
        int i = 1;
        for (std::list<std::string>::const_iterator iter = args.begin(); iter != args.end(); iter++)
        {
            argBuffer[i] = strdup(iter->c_str());
            ++i;
        }
        argBuffer[i] = 0;

        if (execvp(executable.c_str(),argBuffer) == -1)
        {
            MPLOG_ERROR << "error starting child: " + std::string(strerror(errno));
            exit(RunFailed);
        }
    }
    else
    {
        MPLOG_INFO << "Started child process " + boost::lexical_cast<std::string>(child);
    }
    return child;
}
#endif

#ifdef PLATFORM_UNIX
int _runSyncUnix(const std::string& executable,
                                const std::list<std::string>& args)
{
    PROCESS_PID pid = _runAsyncUnix(executable,args);
    int status = 0;
    if (waitpid(pid,&status,0) != -1)
    {
        if (WIFEXITED(status))
        {
            return static_cast<char>(WEXITSTATUS(status));
        }
        else
        {
            MPLOG_ERROR << "Child exited abnormally";
            return -1;
        }
    }
    else
    {
        MPLOG_ERROR << "Failed to get exit status of child " + boost::lexical_cast<std::string>(pid);
        return WaitFailed;
    }
}
#endif






#ifdef PLATFORM_WINDOWS

/**
 * @brief 在windows中运行程序
 * @param _executable
 * @param _args
 * @param runMode
 * @return
 */
int _runWindows(const std::string& _executable,
                               const std::list<std::string>& _args,
                               RunMode runMode)
{
    //	 most Windows API functions allow back and forward slashes to be
    //	 used interchangeably.  However, an application started with
    //	 CreateProcess() may fail to find Side-by-Side library dependencies
    //	 in the same directory as the executable if forward slashes are
    //	 used as path separators, so convert the path to use back slashes here.

    //	 This may be related to LoadLibrary() requiring backslashes instead
    //	 of forward slashes.
    std::string executable = _toWindowsPathSeparators(_executable);

    std::list<std::string> args(_args);
    args.push_front(executable);
    std::string commandLine = _quoteArgs(args);

    STARTUPINFOA startupInfo;
    ZeroMemory(&startupInfo,sizeof(startupInfo));
    startupInfo.cb = sizeof(startupInfo);

    PROCESS_INFORMATION processInfo;
    ZeroMemory(&processInfo,sizeof(processInfo));

    char* commandLineStr = NULL;
    commandLineStr = strdup(commandLine.c_str());
    bool result = CreateProcessA(
        executable.c_str(),
        commandLineStr,
        0 /* process attributes */,
        0 /* thread attributes */,
        false /* inherit handles */,
        NORMAL_PRIORITY_CLASS /* creation flags */,
        0 /* environment */,
        0 /* current directory */,
        &startupInfo /* startup info */,
        &processInfo /* process information */
    );
    free(commandLineStr);

    if (!result)
    {
        MPLOG_ERROR << "Failed to start child process. " << executable + " Last error: " << boost::lexical_cast<std::string>(GetLastError());
        return RunFailed;
    }
    else
    {
        //如果需要等待程序运行完成，那么则执行如下代码
        if (runMode == RunSync)
        {
            if (WaitForSingleObject(processInfo.hProcess,INFINITE) == WAIT_OBJECT_0)
            {
                DWORD status = WaitFailed;
                if (GetExitCodeProcess(processInfo.hProcess,&status) != 0)
                {
                    MPLOG_ERROR << "Failed to get exit code for process. " << executable ;
                }
                return status;
            }
            else
            {
                MPLOG_ERROR << "Failed to wait for process to finish. " << executable ;
                return WaitFailed;
            }
        }
        else
        {
            // 进程正在异步运行-返回0
            return 0;
        }
    }
}
#endif



#ifdef PLATFORM_WINDOWS

/**
 * @brief 在windows中以管理员身份运行程序
 * @param executable
 * @param arguments
 * @return
 */
int _runElevatedWindows(const std::string& executable,
                            const std::list<std::string>& arguments)
{
    std::string args = _quoteArgs(arguments);

    SHELLEXECUTEINFOA executeInfo;
    ZeroMemory(&executeInfo,sizeof(executeInfo));
    executeInfo.cbSize = sizeof(SHELLEXECUTEINFOA);
    executeInfo.fMask = SEE_MASK_NOCLOSEPROCESS;
    // request UAC elevation
    executeInfo.lpVerb = "runas";
    executeInfo.lpFile = executable.c_str();
    executeInfo.lpParameters = args.c_str();
    executeInfo.nShow = SW_SHOWNORMAL;

    MPLOG_INFO << "正在Windows中尝试以管理员的身份运行程序，程序路径：" << executable ;

    if (!ShellExecuteExA(&executeInfo))
    {
        MPLOG_ERROR << "无法通过ShellExecuteEx使程序以管理员身份运行，程序路径：" << executable ;
        return RunElevatedFailed;
    }

    WaitForSingleObject(executeInfo.hProcess, INFINITE);

    return 0;
}
#endif

#if defined(PLATFORM_LINUX) || defined(PLATFORM_FREEBSD)

int _runElevatedLinux(const std::string& executable,
                                   const std::list<std::string>& args,
                                   const std::string& _task)
{
    std::string task(_task);
    if (task.empty())
    {
        task = MP::FileUtils::get_file_name(executable);
    }

    // try available graphical sudo instances until we find one that works.
    // The different sudo front-ends have different behaviors with respect to error codes:
    //
    // - 'kdesudo': return 1 if the user enters the wrong password 3 times or if
    //              they cancel elevation
    //
    // - recent 'gksudo' versions: return 1 if the user enters the wrong password
    //                           : return -1 if the user cancels elevation
    //
    // - older 'gksudo' versions : return 0 if the user cancels elevation

    std::vector<std::string> sudos;

    if (getenv("KDE_SESSION_VERSION"))
    {
        sudos.push_back("kdesudo");
    }
    sudos.push_back("gksudo");

    for (size_t i=0; i < sudos.size(); i++)
    {
        const std::string& sudoBinary = sudos.at(i);

        std::list<std::string> sudoArgs;
        sudoArgs.push_back("-u");
        sudoArgs.push_back("root");

        if (sudoBinary == "kdesudo")
        {
            sudoArgs.push_back("-d");
            sudoArgs.push_back("--comment");
            std::string sudoMessage = task + " needs administrative privileges.  Please enter your password.";
            sudoArgs.push_back(sudoMessage);
        }
        else if (sudoBinary == "gksudo")
        {
            sudoArgs.push_back("--description");
            sudoArgs.push_back(task);
        }
        else
        {
            sudoArgs.push_back(task);
        }

        sudoArgs.push_back("--");
        sudoArgs.push_back(executable);
        std::copy(args.begin(),args.end(),std::back_inserter(sudoArgs));

        int result = runSync(sudoBinary,sudoArgs);


        MPLOG_INFO << "Tried to use sudo " + sudoBinary + " with response " + boost::lexical_cast<std::string>(result);

        if (result != RunFailed)
        {
            return result;
            break;
        }
    }
    return RunElevatedFailed;
}
#endif

#ifdef PLATFORM_MAC

// suppress warning about AuthorizationExecuteWithPriviledges
// being deprecated since OS X 10.7
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
int _runElevatedMac(const std::string& executable,
                        const std::list<std::string>& args)
{
    // request elevation using the Security Service.
    //
    // This only works when the application is being run directly
    // from the Mac.  Attempting to run the app via a remote SSH session
    // (for example) will fail with an interaction-not-allowed error

    OSStatus status;
    AuthorizationRef authorizationRef;

    status = AuthorizationCreate(
            NULL,
            kAuthorizationEmptyEnvironment,
            kAuthorizationFlagDefaults,
            &authorizationRef);

    AuthorizationItem right = { kAuthorizationRightExecute, 0, NULL, 0 };
    AuthorizationRights rights = { 1, &right };

    AuthorizationFlags flags = kAuthorizationFlagDefaults |
            kAuthorizationFlagInteractionAllowed |
            kAuthorizationFlagPreAuthorize |
            kAuthorizationFlagExtendRights;

    if (status == errAuthorizationSuccess)
    {
        status = AuthorizationCopyRights(authorizationRef, &rights, NULL,
                flags, NULL);

        if (status == errAuthorizationSuccess)
        {
            char** argv;
            argv = (char**) malloc(sizeof(char*) * args.size() + 1);

            unsigned int i = 0;
            for (std::list<std::string>::const_iterator iter = args.begin(); iter != args.end(); iter++)
            {
                argv[i] = strdup(iter->c_str());
                ++i;
            }
            argv[i] = NULL;

            FILE* pipe = NULL;

            char* tool = strdup(executable.c_str());

            status = AuthorizationExecuteWithPrivileges(authorizationRef, tool,
                    kAuthorizationFlagDefaults, argv, &pipe);

            if (status == errAuthorizationSuccess)
            {
                // AuthorizationExecuteWithPrivileges does not provide a way to get the process ID
                // of the child process.
                //
                // Discussions on Apple development forums suggest two approaches for working around this,
                //
                // - Modify the child process to sent its process ID back to the parent via
                //   the pipe passed to AuthorizationExecuteWithPrivileges.
                //
                // - Use the generic Unix wait() call.
                //
                // This code uses wait(), which is simpler, but suffers from the problem that wait() waits
                // for any child process, not necessarily the specific process launched
                // by AuthorizationExecuteWithPrivileges.
                //
                // Apple's documentation (see 'Authorization Services Programming Guide') suggests
                // installing files in an installer as a legitimate use for
                // AuthorizationExecuteWithPrivileges but in general strongly recommends
                // not using this call and discusses a number of other alternatives
                // for performing privileged operations,
                // which we could consider in future.

                int childStatus;
                pid_t childPid = wait(&childStatus);

                if (childStatus != 0)
                {
                    MPLOG_ERROR << "elevated process failed with status " + intToStr(childStatus) + " pid " + intToStr(childPid);
                }
                else
                {
                    MPLOG_INFO << "elevated process succeded with pid " + intToStr(childPid));
                }

                return childStatus;
            }
            else
            {
                MPLOG_ERROR << "failed to launch elevated process " + intToStr(status);
                return RunElevatedFailed;
            }

            // If we want to know more information about what has happened:
            // http://developer.apple.com/mac/library/documentation/Security/Reference/authorization_ref/Reference/reference.html#//apple_ref/doc/uid/TP30000826-CH4g-CJBEABHG
            free(tool);
            for (i = 0; i < args.size(); i++)
            {
                free(argv[i]);
            }
        }
        else
        {
            MPLOG_ERROR << "无法获取启动提升进程的权限。状态：" + intToStr(status));

            return RunElevatedFailed;
        }
    }
    else
    {
        return RunElevatedFailed;
    }
}
#pragma clang diagnostic pop
#endif




} //private member



/**
 * @brief 启动一个指定的应用程序，并等待应用程序执行完成，返回退出代码
 * @param executable 待运行的应用程序绝对路径
 * @param args 程序运行参数
 * @return 程序运行后的退出代码。如果程序无法运行则返回-1
 */
int runSync(const std::string& executable,const std::list<std::string>& args){
#ifdef PLATFORM_UNIX
    return _runSyncUnix(executable,args);
#else
    return _runWindows(executable,args,RunSync);
#endif
}

/**
 * @brief 启动一个指定的应用程序后立即返回，不用等待程序执行完成
 * @param executable 待运行的应用程序绝对路径
 * @param args 程序运行参数
 */
void runAsync(const std::string& executable,const std::list<std::string>& args){
#ifdef PLATFORM_WINDOWS
    _runWindows(executable,args,RunAsync);
#elif defined(PLATFORM_UNIX)
    _runAsyncUnix(executable,args);
#endif
}

/**
 * @brief 已管理员身份运行一个指定的应用程序
 * @param executable
 * @param args
 * @param task
 * @return 如果程序不能以管理员身份运行则返回255
 */
int runElevated(const std::string& executable,
                       const std::list<std::string>& args,
                       const std::string& task){
#ifdef PLATFORM_WINDOWS
    (void)task;
    return _runElevatedWindows(executable,args);
#elif defined(PLATFORM_MAC)
    (void)task;
    return _runElevatedMac(executable,args);
#elif defined(PLATFORM_LINUX) || defined(PLATFORM_FREEBSD)
    return _runElevatedLinux(executable,args,task);
#endif
}

/**
 * @brief 等待指定进程退出
 * @param pid 进程Id
 * @return 如果进程存在且已退出则返回true,否则返回false
 */
bool waitForProcess(PROCESS_PID pid){

#ifdef PLATFORM_UNIX
    pid_t result = ::waitpid(pid, 0, 0);
    if (result < 0)
    {
        MPLOG_ERROR << "waitpid() failed with error: " << std::string(strerror(errno));
    }
    return result > 0;
#elif defined(PLATFORM_WINDOWS)
    HANDLE hProc;

    if (!(hProc = OpenProcess(SYNCHRONIZE, FALSE, pid)))
    {
        MPLOG_ERROR << "无法根据进程id " + boost::lexical_cast<std::string>(pid) + " 获得对应的进程句柄。错误码 " + boost::lexical_cast<std::string>(GetLastError());
        return false;
    }

    DWORD dwRet = WaitForSingleObject(hProc, INFINITE);
    CloseHandle(hProc);

    if (dwRet == WAIT_FAILED)
    {
        MPLOG_ERROR << "WaitForSingleObject 函数执行失败，错误码： " + boost::lexical_cast<std::string>(GetLastError());
    }

    return (dwRet == WAIT_OBJECT_0);
#endif

}

#ifdef PLATFORM_WINDOWS
/**
 * @brief 转换windows命令行参数
 * @param commandLine
 * @param argc
 * @param argv
 */
void convertWindowsCommandLine(LPCWSTR commandLine, int& argc, char**& argv){
    argc = 0;
    LPWSTR* argvUnicode = CommandLineToArgvW(commandLine,&argc);

    argv = new char*[argc];
    for (int i=0; i < argc; i++)
    {
        const int BUFFER_SIZE = 4096;
        char buffer[BUFFER_SIZE];

        int length = WideCharToMultiByte(CP_ACP,
          0 /* flags */,
          argvUnicode[i],
          -1, /* argvUnicode is null terminated */
          buffer,
          BUFFER_SIZE,
          0,
          FALSE);

        // note: if WideCharToMultiByte() fails it will return zero,
        // in which case we store a zero-length argument in argv
        if (length == 0)
        {
            argv[i] = new char[1];
            argv[i][0] = '\0';
        }
        else
        {
            // if the input string to WideCharToMultiByte is null-terminated,
            // the output is also null-terminated
            argv[i] = new char[length];
            strncpy(argv[i],buffer,length);
        }
    }
    LocalFree(argvUnicode);
}

#endif



}   //END ProcessUtils
}   //END MP








